# -*- coding:Utf-8 -*-
'''
Created on 27 nov. 2012

@author: fefoley
'''


from datetime import *
import collections 
from pyicl import IntInterval, IntervalBounds, IntIntervalSet



'--------------------------------------------------------------------------------------'
'                           DatetimeInterval                                           '
'--------------------------------------------------------------------------------------'

class DatetimeInterval(object):
    """
    A datetime interval coded with integer.
    self.dtstart: start datetime
    self.dtend: end datetime
    self._intinterval: an IntInterval for computation
    ORDINAL_MIN: datetime has to be more than
    ORDINAL_MAX: datetime has to be less than
    BASE: to convert datetime to and from integer (not base10, not base2, but baseTIme :-))
    """
    'limit to 2000 - 2050'
    ORDINAL_MIN = date(2000, 1, 1).toordinal()
    ORDINAL_MAX = date(2050, 12, 31).toordinal()
    BASE = [24*60*60, 60*60, 60, 1]
    
    def __init__(self, dtstart=None, dtend=None):
        'if None, it is because we want an empty one'
        if not dtstart or not dtend:
            self.dtstart = None
            self.dtend = None
            self._intinterval = None
            return
        
        'check we are in limits'
        if dtstart.toordinal() < self.ORDINAL_MIN or dtend.toordinal() > self.ORDINAL_MAX:
            raise Exception("datetimes out of 2000/01/01 - 2050/12/31 range")
        
        if dtstart > dtend:
            dtstart, dtend = dtend, dtstart
        
        self.dtstart = dtstart
        self.dtend = dtend
        intstart = self.tointeger(dtstart)
        intend = self.tointeger(dtend)
        self._intinterval = IntInterval(intstart, intend)

    @classmethod
    def tointeger(cls, dt):
        """Class function to convert a datetime into an interger.    """
        number = [dt.toordinal() - cls.ORDINAL_MIN, dt.hour, dt.minute, dt.second]
        return sum(b * n for b, n in zip(cls.BASE, number))
    
    @classmethod
    def frominteger(cls, i):
        """Class function to get a datetime from an integer.   """
        number = []
        for b in cls.BASE:
            number.append(i // b)
            i %= b
        return datetime.combine(date.fromordinal(number[0] + cls.ORDINAL_MIN),
                                time(number[1], number[2], number[3]))
    
    def gettimedelta(self):
        """return the interval size as datetime.timedelta.   """
        return self.dtend - self.dtstart
    
    
    def getbounds(self):
        """Returns the (datetime start and end bounds.   """
        return (self.dtstart, self.dtend)
        
    def __add__(self, other):
        """Function that call hull if self and other intersects or touches.   """
        if self.intersects(other) or self.touches(other):
            i = self._intinterval.hull(other._intinterval)
            dtscope = DatetimeInterval()   #empty DatetimeInterval
            dtscope._intinterval = i
            dtscope.dtstart = min(self.dtstart, other.dtstart)
            dtscope.dtend = max(self.dtend, other.dtend)
            return dtscope
        return None

    def __and__(self, other):
        """Function to intersects self and other and return a new DatetimeInterval.   """
        interval = self._intinterval & other._intinterval
        dtintersect = DatetimeInterval()
        dtintersect._intinterval = interval
        dtintersect.dtstart = min(self.dtend, other.dtend)
        dtintersect.dtend = max(self.dtstart, other.dtstart) 
        return dtintersect                 

    def intersects(self, other):
        """Function to check for if self intersects with other.   """
        return self._intinterval.intersects(other._intinterval)
    
    def touches(self, other):
        """Function to check for if self touches other.   """        
        return self._intinterval.touches(other._intinterval) or other._intinterval.touches(self._intinterval)
    
    def length(self):
        if self.dtstart and self.dtend:
            return self.dtend - self.dtstart
        else :
            raise Exception("{0} has no dtstart and/or dtend defined".format(self))
        
    
    def __lt__(self, other): return self._intinterval.__lt__(other._intinterval)
    def __le__(self, other): return self._intinterval.__le__(other._intinterval)
    def __eq__(self, other): return self._intinterval.__eq__(other._intinterval)
    def __ne__(self, other): return self._intinterval.__ne__(other._intinterval)
    def __gt__(self, other): return self._intinterval.__gt__(other._intinterval)
    def __ge__(self, other): return self._intinterval.__ge__(other._intinterval)    

    def __str__(self):
        dic_bounds2str = {str(IntervalBounds.OPEN): ("(", ")"), str(IntervalBounds.LEFT_OPEN): ("(", "]"), 
                      str(IntervalBounds.RIGHT_OPEN): ("[",")"), str(IntervalBounds.CLOSED): ("[", "]")}
        s1, s2 = dic_bounds2str[str(self._intinterval.bounds)]
        return s1 + str(self.dtstart) + ", " + str(self.dtend) + s2

    def __repr__(self):
        return self.__str__() + '\n'


'--------------------------------------------------------------------------------------'
'                           DatetimeIntervalSet                                        '
'--------------------------------------------------------------------------------------'

class DatetimeIntervalSet(collections.MutableSet):
    """
    self._intervals: mutable set of intervals that aggregate on overlap or on touch
    """
    
    def __init__(self, iterable=[]):
        self._intervals = set([])
        self.add(iterable)
                          
    def __contains__(self, value):
        return value in self._intervals
    
    def __iter__(self):
        return iter(self._intervals)
    
    def __len__(self):
        return len(self._intervals)

    def add(self, iterable):
        'check if a single element is given'
        if not isinstance(iterable, collections.Container):
            iterable = (iterable, )
        'iterate to append the values'
        lst = self._intervals            
        for value in iterable:
            'check is it a DatetimeInterval'
            if not isinstance(value, DatetimeInterval):
                raise Exception("trying to add {0} to {1}".format(value, type(self)))
            
            'compare with current element and aggregate on overlap or touches'
            'if not empty, loop through all current elements and check where it overlaps'            
            if not lst:
                lst.add(value)
            else :
                overlap_lst = []
                for current_value in lst:
                    if current_value.intersects(value) or current_value.touches(value):
                        overlap_lst.append(current_value)
                        
                'simple append if it never overlaps/touches'
                'otherwise aggregate with overlap_values, remove overlap_values, append new_aggregate_value'                
                if not overlap_lst:
                    lst.add(value)
                else:
                    new_value = reduce(lambda x,y: x+y, [value] + overlap_lst)
                    for v in overlap_lst:
                        lst.remove(v)
                    lst.add(new_value)

    def insert(self, iterable):
        """Function to insert without aggregating.   """
        'check if a single element is given'
        if not isinstance(iterable, collections.Container):
            iterable = (iterable, )
        'iterate to append the values'        
        for value in iterable:
            'check is it a DatetimeInterval'
            if not isinstance(value, DatetimeInterval):
                raise Exception("trying to add {0} to {1}".format(value, type(self)))
            self._intervals.add(value)        

    def aggregate(self):
        """
        Function to loop through the set and aggregate the DatetimeIntervals.
        To be used after DatetimeInterval.insert(DatetimeInteval()).
        """
        'stop if empty set'
        if not self._intervals: return        
        'compare 2 by 2 intervals and aggregate'
        lst1 = self._intervals.copy()  # start with a fresh, updated copy
        while lst1:
            value1 = lst1.pop()        # take a value 
            lst2 = lst1.copy()
            while lst2:
                value2 = lst2.pop()
                if value1.intersects(value2) or value1.touches(value2):
                    value_aggreg = value1 + value2
                    self._intervals.discard(value1)
                    self._intervals.discard(value2)
                    self._intervals.add(value_aggreg)
                    lst1.discard(value2)      # should not consider this discarded value as value1
                    lst1.add(value_aggreg)    # should consider this new value as value1 for latter intersection
                    lst2 = []                 # stopping here in value2 looping since value2 is not an option anymore
                       
    def __and__(self, other):
        """Function to intersect with another DatetimeIntervalSet.   """
        'check if a single element is given'
        if not isinstance(other, collections.Container):
            other = (other, )

        dtand = DatetimeIntervalSet([])                    
        'loop through current values and check for intersects with all other values'
        for current_value in self._intervals:
            'loop through all other values and check for intersects with the current value'
            for other_value in other:
                'check is it a DatetimeInterval'
                if not isinstance(other_value, DatetimeInterval):
                    raise Exception("trying to intersect {0} with {1}".format(other_value, type(self)))                
                
                'if current value intersects with other value, add the intersection to the intersection DatetimeIntervalSet'
                if current_value.intersects(other_value) or current_value.touches(other_value):
                    dtand.insert(current_value & other_value)   # using insert, and not add --> doesnt aggregate
        return dtand
    
    def discard(self, iterable):
        'check if a single element is given'
        if not isinstance(iterable, collections.Container):
            iterable = (iterable, )
        
        'loop through the elements to discard'
        for value in iterable:
            self._intervals.discard(value)
    
    def __str__(self):
        return str(self._intervals)


'--------------------------------------------------------------------------------------'
'                           Timestrip                                                  '
'--------------------------------------------------------------------------------------'    

class Timestrip(DatetimeInterval):
    """
    DatetimeInterval class holding a content
    self.content
    """
    
    def __init__(self, dtstart, dtend, content):  
        """Init with a DatetimeInterval and a Content.   """   
        super(Timestrip, self).__init__(dtstart, dtend)
        self.content = content        
 
    def __add__(self, other):
        """
        Function to implement timestrip1 + timestrip2, taking into account the content.   
        Add only if the content is addable.
        """
        'check if contents are addable'
        added_content = self.content + other.content
        if not added_content:
            raise NotImplementedError("notaddable + addable not implemented") 
        'then, add datetimeInterval and Content into a new Timestrip'
        # UGLY! But dont know how to get a Timestrip instance instead of parent DatetimeInterval instance
        dtinterval = super(Timestrip, self).__add__(other)   
        return Timestrip(dtinterval.dtstart, dtinterval.dtend, added_content)
        
    def __and__(self, other):
        """Function to implement timestrip1 & timestrip2, adding contents on overlap.   """
        'if it is a simple DatetimeInterval, keep going with Timestrip with empty content'    
        if isinstance(other, DatetimeInterval):
            other = Timestrip(other.dtstart, other.dtend, Content())
        
        'first, if intervals does not overlap, set content to None'
        dtintersection = super(Timestrip, self).__and__(other)
        if not dtintersection._intinterval:            
            return Timestrip(dtintersection.dtstart, dtintersection.dtend, Content())   # ???????????
        
        'second, both contents are addable'
        if self.content.isaddable() and other.content.isaddable():
            return Timestrip(dtintersection.dtstart, dtintersection.dtend, self.content + other.content)
        
        
        raise NotImplementedError("notaddable & addable not implemented") 
 
       
#        
#        'then check if contents are addable - if not, raise an error'   # for now, error. Latter, might cut one or other Timestrip
#        if not self.content.isaddable() or not other.content.isaddable():
#            raise Exception("trying to intersect Timestrips with not-addable contents\nt1={0} - t2={1}".format(self, other))
#        
#        # and now ?
        
    def __str__(self):
        str_interval = super(Timestrip, self).__str__()
        return str_interval + " - " + str(type(self.content))

    def __repr__(self):
        return self.__str__() + '\n'        
        
         
'--------------------------------------------------------------------------------------'
'                           Content                                                    '
'--------------------------------------------------------------------------------------'
    
        
class Content(object):
    """
    Content base class from which inherit SentMail, SentMeeting, Meeting, Lunch, GoingtoWork, ...
    self._isaddable: True/False  can it be aggregated with another content?
    self._label : string that describes what was going on in this time frame
    self._contentlst : list of all contents in the same time frame, from aggregation 
    """
    
    def __init__(self, label="", obj=None, isaddable = True):
        self._isaddable = isaddable
        self._label = label
        self._contentlst = []
        if obj: self._contentlst.append(obj) 

    def isaddable(self):
        return self._isaddable

    def __add__(self, other):
        """
        Standard content1 + content2, checking for addability. 
        Private__add_() must be implemented
        """
        if not self._isaddable or not other._isaddable:
            return 
        if not other._contentlst:
            return self
        else :
            return self._add(other)
        
    def _add(self, addable):
        """Function to implement to add two addable content.   """
        new_content = Content()
        new_content._contentlst = self._contentlst + addable._contentlst
        new_content._label = self._label + '\n' + addable._label
        return new_content

        
#        if self._contentlst and addable._contentlst:
#            new_content = Content()
#            new_content._contentlst = self._contentlst + addable._contentlst
#            new_content._label = self._label + '\n' + addable._label
#            return new_content
#        else :
#            pass
        
#        raise NotImplementedError("Please implement this method in {0}".format(type(self)))
    
    def setlabel(self):
        raise NotImplementedError("Please implement this method in {0}".format(type(self)))   
    
    def getlabel(self):
        return self._label

'--------------------------------------------------------------------------------------'
'                           TimestripSet                                               '
'--------------------------------------------------------------------------------------'
 

class TimestripSet(DatetimeIntervalSet):
    """
    Set of Timestrip, that implement specific method for Timestrip
    """
    
    def __init__(self, iterable=[]):
        super(TimestripSet, self).__init__(iterable)
    
#    def add(self, iterable):
#        """Add Timestrip to the set, checking and computing aggregation if required.   """
#        'check if a single element is given'
#        if not isinstance(iterable, collections.Container):
#            iterable = (iterable, )
#        'iterate to append the values'
#        lst = self._intervals            
#        for value in iterable:
#            'check is it a DatetimeInterval'
#            if not isinstance(value, Timestrip):
#                raise Exception("trying to add {0} to {1}".format(value, type(self)))
#            
#            'compare with current element and aggregate on overlap or touches'
#            'if not empty, loop through all current elements and check where it overlaps'            
#            if not lst:
#                lst.add(value)
#            else :
#                overlap_lst = []
#                for current_value in lst:
#                    if not current_value or not value:
#                        pass
#                    if current_value.intersects(value) or current_value.touches(value):
#                        overlap_lst.append(current_value)
#                        
#                'simple append if it never overlaps/touches'
#                'otherwise aggregate with overlap_values, remove overlap_values, append new_aggregate_value'                
#                if not overlap_lst:
#                    lst.add(value)
#                else:
#                    new_value = value
#                    for v in overlap_lst:
#                        if v.content.isaddable():
#                            new_value = new_value + v
#                            lst.remove(v)
#                    lst.add(new_value)


    def __and__(self, other):
        """Function to intersect with another TimestripSet/DatetimeIntervalSet.   """
        'check if a single element is given'
        if not isinstance(other, collections.Container):
            other = (other, )

        dtand = TimestripSet([])                    
        'loop through current values and check for intersects with all other values'
        for current_value in self:
            'loop through all other values and check for intersects with the current value'
            for other_value in other:
                'check is it a DatetimeInterval'
                if not isinstance(other_value, DatetimeInterval):
                    raise Exception("trying to intersect {0} with {1}".format(other_value, type(self)))                
                
                'if current value intersects with other value, add the intersection to the intersection DatetimeIntervalSet'
                if current_value.intersects(other_value) or current_value.touches(other_value):
                    dtand.insert(current_value & other_value)   # using insert, and not add --> doesnt aggregate
        return dtand

    def aggregate(self):
        """
        Function to loop through the set and aggregate the Timestrip.
        To be used after TimestripSet.insert(Timestrip()).
        """
        'stop if empty set'
        if not self._intervals: return        
        'compare 2 by 2 intervals and aggregate'
        lst1 = self._intervals.copy()  # start with a fresh, updated copy
        while lst1:
            value1 = lst1.pop()        # take a value 
            if value1.content.isaddable():
                lst2 = lst1.copy()
                while lst2:
                    value2 = lst2.pop()
                    if value2.content.isaddable():
                        if value1.intersects(value2) or value1.touches(value2):
                            value_aggreg = value1 + value2
                            self._intervals.discard(value1)
                            self._intervals.discard(value2)
                            self._intervals.add(value_aggreg)
                            lst1.discard(value2)      # should not consider this discarded value as value1
                            lst1.add(value_aggreg)    # should consider this new value as value1 for latter intersection
                            lst2 = []                 # stopping here in value2 looping since value2 is not an option anymore



#class TimeStrip(object):
#    """
#    Represents a period of time, in which one or more tasks were undergone
#    self.start : start time of the time strip - typeof(datetime.datetime)
#    self.end : end - typeof(datetime.datetime)
#    self._interval : [start, end]
#    self.kindof : MAIL, MEETING, DEADTIME, ...
#    self.flag : description string
#    self.evidences : liste of evidences (Mail, Meeting, ...)
#    self.label : starting with "start-end : flag", can aggregate several flags
#    self.adherence : in minute. number of minutes max by which the timestrip can aggregate to another
#    """
#    MAIL = 'mail'
#    MEETING = 'meeting'
#    PARENT = "parent"
#    DEADTIME = "deadtime"
#    
#    def __init__(self, _interval, kindof, flag, evidence, label = "", adherence = 0, children =[]):
#        self._interval = _interval
#        self.start = _interval.lower
#        self.end = _interval.upper
#        self.kindof = kindof
#        self.flag = flag
#        self.evidences = [evidence]
#        prefix = ''   # I want a prefix to the label for meeting 
#        if isinstance(evidence, Meeting):
#            prefix = '(' + evidence.BusyStatus + ')'
#        if label == '': 
#            self.label = (prefix + 
#                          self._interval.lower.strftime("%H:%M") + '-' +
#                          self._interval.upper.strftime("%H:%M") + ': ' +
#                          self.flag)               
#        else : 
#            self.label = label
#        self.adherence = adherence
#        self.children = children
#    
#    @classmethod
#    def _fusion(cls, child1, child2):
#        """Function to merge two TimeStrip into a new parent TimeStrip linking to them.  """
#        _interval = child1._interval.hull(child2._interval)   #merge _interval
#        kindof = cls.PARENT
#        flag = cls.PARENT
#        evidences = [e for e in child1.evidences] + [e for e in child2.evidences]   #merge evidences
#        label = child1.label + '\n' + child2.label                  #merge labels
#        adherence = max(child1.adherence, child2.adherence)        ################## what does this meann ? ###########
#        children = [child1, child2]     ############ !!!!!!!!!!!  --> get recursively the deeper childer ?
#        return TimeStrip(_interval, kindof, flag, evidences, label, adherence, children)
#
#    @classmethod
#    def aggreg(cls, lst, i=0, j=1):
#        """
#        Aggregation function, that takes a list of TimeStrip and conduct
#        aggregation iterations among them. If two TimeStrip are closer in time
#        than their adherence variables, they merge, creating a parent TimeStrip that
#        takes the children place in the list, and links to them
#        return the modified list
#        """     
#        if i >= len(lst)-1:  # end comparison
#            return lst
#        if j >=len(lst):  # time to increment i
#            i = i + 1
#            j = i + 1
#            return TimeStrip.aggreg(lst, i, j)
#        'compare deux elements et eventuellement fusionne'
#        dist = lst[i]._interval.distance(lst[j]._interval)
#        if not dist or dist < timedelta(minutes=max(lst[i].adherence, lst[j].adherence)):  #merge  
#            lst[i] = cls._fusion(lst[i], lst[j])
#            del lst[j]
#            return TimeStrip.aggreg(lst, i, j)
#        # no merge, take next j
#        return TimeStrip.aggreg(lst, i, j+1)  
#        
#        
#    def __str__(self):
#        return self.start.strftime("%H:%M") + '-' + self.end.strftime("%H:%M") +':\n\t' + self.label.replace('\n','\n\t') 
#    
#    def __repr__(self):
#        return self.start.strftime("%H:%M") + '-' + self.end.strftime("%H:%M") +':\n\t' + self.label.replace('\n','\n\t') + '\n'
       
        
        
        